import collection.mutable

/** A framework for defining stackable entry point wrappers */
object EntryPoint:

  /** A base trait for wrappers of entry points.
  *  Sub-traits: Annotation#Wrapper
  *              Adapter#Wrapper
  */
  sealed trait Wrapper

  /** This class provides a framework for compiler-generated wrappers
  *  of "entry-point" methods. It routes and transforms parameters and results
  *  between a compiler-generated wrapper method that has calling conventions
  *  fixed by a framework and a user-written entry-point method that can have
  *  flexible argument lists. It allows the wrapper to provide help and usage
  *  information as well as customised error messages if the actual wrapper arguments
  *  do not match the expected entry-point parameters.
  *
  *  The protocol of calls from the wrapper method is as follows:
  *
  *    1. Create a `call` instance with the wrapper argument.
  *    2. For each parameter of the entry-point, invoke `call.nextArgGetter`,
  *       or `call.finalArgsGetter` if is a final varargs parameter.
  *    3. Invoke `call.run` with the closure of entry-point applied to all arguments.
  *
  *  The wrapper class has this outline:
  *
  *     object <wrapperClass>:
  *       @WrapperAnnotation def <wrapperMethod>(args: <Argument>) =
  *         ...
  *
  *  Here `<wrapperClass>` and `<wrapperMethod>` are obtained from an
  *  inline call to the `wrapperName` method.
  */
  trait Annotation extends annotation.StaticAnnotation:

    /** The class used for argument parsing. E.g. `scala.util.FromString`, if
    *  arguments are strings, but it could be something else.
    */
    type ArgumentParser[T]

    /** The required result type of the user-defined main function */
    type EntryPointResult

    /** The fully qualified name (relative to enclosing package) to
    *  use for the static wrapper method.
    *  @param entryPointName the fully qualified name of the user-defined entry point method
    */
    inline def wrapperName(entryPointName: String): String

    /** Create an entry point wrapper.
    *  @param entryPointName the fully qualified name of the user-defined entry point method
    *  @param docComment     the doc comment of the user-defined entry point method
    */
    def wrapper(entryPointName: String, docComment: String): Wrapper

    /** Base class for descriptions of an entry point wrappers */
    abstract class Wrapper extends EntryPoint.Wrapper:

      /** The type of the wrapper argument. E.g., for Java main methods: `Array[String]` */
      type Argument

      /** The return type of the generated wrapper. E.g., for Java main methods: `Unit` */
      type Result

      /** An annotation type with which the wrapper method is decorated.
      *  No annotation is generated if the type is left abstract.
      *  Multiple annotations are generated if the type is an intersection of annotations.
      */
      type WrapperAnnotation <: annotation.Annotation

      /** The fully qualified name of the user-defined entry point method that is wrapped */
      val entryPointName: String

      /** The doc comment of the user-defined entry point method that is wrapped */
      val docComment: String

      /** A new wrapper call with arguments from `args` */
      def call(arg: Argument): Call

      /** A class representing a wrapper call */
      abstract class Call:

        /** The getter for the next argument of type `T` */
        def nextArgGetter[T](argName: String, fromString: ArgumentParser[T], defaultValue: Option[T] = None): () => T

        /** The getter for a final varargs argument of type `T*` */
        def finalArgsGetter[T](argName: String, fromString: ArgumentParser[T]): () => Seq[T]

        /** Run `entryPointWithArgs` if all arguments are valid,
        *  or print usage information and/or error messages.
        *  @param entryPointWithArgs the applied entry-point to run
        */
        def run(entryPointWithArgs: => EntryPointResult): Result
      end Call
    end Wrapper
  end Annotation

  /** An annotation that generates an adapter of an entry point wrapper.
  *  An `EntryPoint.Adapter` annotation should always be written together
  *  with an `EntryPoint.Annotation` and the adapter should be given first.
  *  If several adapters are present, they are applied right to left.
  *  Example:
  *
  *      @logged @transactional @main def f(...)
  *
  *  This wraps the main method generated by @main first in a `transactional`
  *  wrapper and then in a `logged` wrapper. The result would look like this:
  *
  *     $logged$wrapper.adapt { y =>
  *       $transactional$wrapper.adapt { z =>
  *         val cll = $main$wrapper.call(z)
  *         val arg1 = ...
  *         ...
  *         val argN = ...
  *         cll.run(...)
  *       } (y)
  *     } (x)
  *
  *  where
  *
  *    - $logged$wrapper, $transactional$wrapper, $main$wrapper are the wrappers
  *      created from @logged, @transactional, and @main, respectively.
  *    - `x` is the argument of the outer $logged$wrapper.
  */
  trait Adapter extends annotation.StaticAnnotation:

    /** Creates a new wrapper around `wrapped` */
    def wrapper(wrapped: EntryPoint.Wrapper): Wrapper

    /** The wrapper class. A wrapper class must define a method `adapt`
    *  that maps unary functions to unary functions. A typical definition
    *  of `adapt` is:
    *
    *     def adapt(f: A1 => B1)(a: A2): B2 = toB2(f(toA1(a)))
    *
    *  This version of `adapt` converts its argument `a` to the wrapped
    *  function's argument type `A1`, applies the function, and converts
    *  the application's result back to type `B2`. `adapt` can also call
    *  the wrapped function only under a condition or call it multiple times.
    *
    *  `adapt` can also be polymorphic. For instance:
    *
    *     def adapt[R](f: A1 => R)(a: A2): R = f(toA1(a))
    *
    *   or
    *
    *     def adapt[A, R](f: A => R)(a: A): R = { log(a); f(a) }
    *
    *  Since `adapt` can be of many forms, the base class does not provide
    *  an abstract method that needs to be implemented in concrete wrapper
    *  classes. Instead it is checked that the types line up when adapt chains
    *  are assembled.
    *
    *  I investigated an explicitly typed approach, but could not arrive at a
    *  solution that was clean and simple enough. If Scala had more dependent
    *  type support, it would be quite straightforward, i.e. generic `Wrapper`
    *  could be defined like this:
    *
    *    class Wrapper(wrapped: EntryPoint.Wrapper):
    *      type Argument
    *      type Result
    *      def adapt(f: wrapped.Argument => wrapped.Result)(x: Argument): Result
    *
    *  But to get this to work, we'd need support for types depending on their
    *  arguments, e.g. a type of the form `Wrapper(wrapped)`. That's an interesting
    *  avenue to pursue. Until that materializes I think it's preferable to
    *  keep the `adapt` type contract implicit (types are still checked when adapts
    *  are generated, of course).
    */
    abstract class Wrapper extends EntryPoint.Wrapper:
      /** The wrapper that this wrapped in turn by this wrapper */
      val wrapped: EntryPoint.Wrapper

      /** The wrapper of the entry point annotation that this wrapper
      *  wraps directly or indirectly
      */
      def finalWrapped: EntryPoint.Annotation#Wrapper = wrapped match
        case wrapped: EntryPoint.Adapter#Wrapper => wrapped.finalWrapped
        case wrapped: EntryPoint.Annotation#Wrapper => wrapped
    end Wrapper
  end Adapter
